<?php
namespace think;

use think\exception\ClassNotFoundException;
class Validate
{
    protected static $instance;
    protected static $type = [];
    protected $alias = ['>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq'];
    protected $rule = [];
    protected $message = [];
    protected $field = [];
    protected static $typeMsg = ['require' => ':attribute不能为空', 'number' => ':attribute必须是数字', 'float' => ':attribute必须是浮点数', 'boolean' => ':attribute必须是布尔值', 'email' => ':attribute格式不符', 'array' => ':attribute必须是数组', 'accepted' => ':attribute必须是yes、on或者1', 'date' => ':attribute格式不符合', 'file' => ':attribute不是有效的上传文件', 'image' => ':attribute不是有效的图像文件', 'alpha' => ':attribute只能是字母', 'alphaNum' => ':attribute只能是字母和数字', 'alphaDash' => ':attribute只能是字母、数字和下划线_及破折号-', 'activeUrl' => ':attribute不是有效的域名或者IP', 'chs' => ':attribute只能是汉字', 'chsAlpha' => ':attribute只能是汉字、字母', 'chsAlphaNum' => ':attribute只能是汉字、字母和数字', 'chsDash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', 'url' => ':attribute不是有效的URL地址', 'ip' => ':attribute不是有效的IP地址', 'dateFormat' => ':attribute必须使用日期格式 :rule', 'in' => ':attribute必须在 :rule 范围内', 'notIn' => ':attribute不能在 :rule 范围内', 'between' => ':attribute只能在 :1 - :2 之间', 'notBetween' => ':attribute不能在 :1 - :2 之间', 'length' => ':attribute长度不符合要求 :rule', 'max' => ':attribute长度不能超过 :rule', 'min' => ':attribute长度不能小于 :rule', 'after' => ':attribute日期不能小于 :rule', 'before' => ':attribute日期不能超过 :rule', 'expire' => '不在有效期内 :rule', 'allowIp' => '不允许的IP访问', 'denyIp' => '禁止的IP访问', 'confirm' => ':attribute和确认字段:2不一致', 'different' => ':attribute和比较字段:2不能相同', 'egt' => ':attribute必须大于等于 :rule', 'gt' => ':attribute必须大于 :rule', 'elt' => ':attribute必须小于等于 :rule', 'lt' => ':attribute必须小于 :rule', 'eq' => ':attribute必须等于 :rule', 'unique' => ':attribute已存在', 'regex' => ':attribute不符合指定规则', 'method' => '无效的请求类型', 'token' => '令牌数据无效', 'fileSize' => '上传文件大小不符', 'fileExt' => '上传文件后缀不符', 'fileMime' => '上传文件类型不符'];
    protected $currentScene = null;
    protected $regex = [];
    protected $scene = [];
    protected $error = [];
    protected $batch = false;
    public function __construct(array $rules = [], $message = [], $field = [])
    {
        $this->rule = array_merge($this->rule, $rules);
        $this->message = array_merge($this->message, $message);
        $this->field = array_merge($this->field, $field);
    }
    public static function make($rules = [], $message = [], $field = [])
    {
        if (is_null(self::$instance)) {
            self::$instance = new self($rules, $message, $field);
        }
        return self::$instance;
    }
    public function rule($name, $rule = '')
    {
        if (is_array($name)) {
            $this->rule = array_merge($this->rule, $name);
        } else {
            $this->rule[$name] = $rule;
        }
        return $this;
    }
    public static function extend($type, $callback = null)
    {
        if (is_array($type)) {
            self::$type = array_merge(self::$type, $type);
        } else {
            self::$type[$type] = $callback;
        }
    }
    public static function setTypeMsg($type, $msg = null)
    {
        if (is_array($type)) {
            self::$typeMsg = array_merge(self::$typeMsg, $type);
        } else {
            self::$typeMsg[$type] = $msg;
        }
    }
    public function message($name, $message = '')
    {
        if (is_array($name)) {
            $this->message = array_merge($this->message, $name);
        } else {
            $this->message[$name] = $message;
        }
        return $this;
    }
    public function scene($name, $fields = null)
    {
        if (is_array($name)) {
            $this->scene = array_merge($this->scene, $name);
        }
        if (is_null($fields)) {
            $this->currentScene = $name;
        } else {
            $this->scene[$name] = $fields;
        }
        return $this;
    }
    public function hasScene($name)
    {
        return isset($this->scene[$name]);
    }
    public function batch($batch = true)
    {
        $this->batch = $batch;
        return $this;
    }
    public function check($data, $rules = [], $scene = '')
    {
        $this->error = [];
        if (empty($rules)) {
            $rules = $this->rule;
        }
        $scene = $this->getScene($scene);
        if (is_array($scene)) {
            $change = [];
            $array = [];
            foreach ($scene as $k => $val) {
                if (is_numeric($k)) {
                    $array[] = $val;
                } else {
                    $array[] = $k;
                    $change[$k] = $val;
                }
            }
        }
        foreach ($rules as $key => $item) {
            if (is_numeric($key)) {
                $key = $item[0];
                $rule = $item[1];
                if (isset($item[2])) {
                    $msg = is_string($item[2]) ? explode('|', $item[2]) : $item[2];
                } else {
                    $msg = [];
                }
            } else {
                $rule = $item;
                $msg = [];
            }
            if (strpos($key, '|')) {
                list($key, $title) = explode('|', $key);
            } else {
                $title = isset($this->field[$key]) ? $this->field[$key] : $key;
            }
            if (!empty($scene)) {
                if ($scene instanceof \Closure && !call_user_func_array($scene, [$key, $data])) {
                    continue;
                } elseif (is_array($scene)) {
                    if (!in_array($key, $array)) {
                        continue;
                    } elseif (isset($change[$key])) {
                        $rule = $change[$key];
                    }
                }
            }
            $value = $this->getDataValue($data, $key);
            if ($rule instanceof \Closure) {
                $result = call_user_func_array($rule, [$value, $data]);
            } else {
                $result = $this->checkItem($key, $value, $rule, $data, $title, $msg);
            }
            if (true !== $result) {
                if (!empty($this->batch)) {
                    if (is_array($result)) {
                        $this->error = array_merge($this->error, $result);
                    } else {
                        $this->error[$key] = $result;
                    }
                } else {
                    $this->error = $result;
                    return false;
                }
            }
        }
        return !empty($this->error) ? false : true;
    }
    protected function checkItem($field, $value, $rules, $data, $title = '', $msg = [])
    {
        if (is_string($rules)) {
            $rules = explode('|', $rules);
        }
        $i = 0;
        foreach ($rules as $key => $rule) {
            if ($rule instanceof \Closure) {
                $result = call_user_func_array($rule, [$value, $data]);
                $info = is_numeric($key) ? '' : $key;
            } else {
                if (is_numeric($key)) {
                    if (strpos($rule, ':')) {
                        list($type, $rule) = explode(':', $rule, 2);
                        if (isset($this->alias[$type])) {
                            $type = $this->alias[$type];
                        }
                        $info = $type;
                    } elseif (method_exists($this, $rule)) {
                        $type = $rule;
                        $info = $rule;
                        $rule = '';
                    } else {
                        $type = 'is';
                        $info = $rule;
                    }
                } else {
                    $info = $type = $key;
                }
                if (0 === strpos($info, 'require') || !is_null($value) && '' !== $value) {
                    $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type];
                    $result = call_user_func_array($callback, [$value, $rule, $data, $field, $title]);
                } else {
                    $result = true;
                }
            }
            if (false === $result) {
                if (isset($msg[$i])) {
                    $message = $msg[$i];
                    if (is_string($message) && strpos($message, '{%') === 0) {
                        $message = Lang::get(substr($message, 2, -1));
                    }
                } else {
                    $message = $this->getRuleMsg($field, $title, $info, $rule);
                }
                return $message;
            } elseif (true !== $result) {
                if (is_string($result) && false !== strpos($result, ':')) {
                    $result = str_replace([':attribute', ':rule'], [$title, (string) $rule], $result);
                }
                return $result;
            }
            $i++;
        }
        return $result;
    }
    protected function confirm($value, $rule, $data, $field = '')
    {
        if ('' == $rule) {
            if (strpos($field, '_confirm')) {
                $rule = strstr($field, '_confirm', true);
            } else {
                $rule = $field . '_confirm';
            }
        }
        return $this->getDataValue($data, $rule) === $value;
    }
    protected function different($value, $rule, $data)
    {
        return $this->getDataValue($data, $rule) != $value;
    }
    protected function egt($value, $rule)
    {
        return $value >= $rule;
    }
    protected function gt($value, $rule)
    {
        return $value > $rule;
    }
    protected function elt($value, $rule)
    {
        return $value <= $rule;
    }
    protected function lt($value, $rule)
    {
        return $value < $rule;
    }
    protected function eq($value, $rule)
    {
        return $value == $rule;
    }
    protected function is($value, $rule, $data = [])
    {
        switch ($rule) {
            case 'require':
                $result = !empty($value) || '0' == $value;
                break;
            case 'accepted':
                $result = in_array($value, ['1', 'on', 'yes']);
                break;
            case 'date':
                $result = false !== strtotime($value);
                break;
            case 'alpha':
                $result = $this->regex($value, '/^[A-Za-z]+$/');
                break;
            case 'alphaNum':
                $result = $this->regex($value, '/^[A-Za-z0-9]+$/');
                break;
            case 'alphaDash':
                $result = $this->regex($value, '/^[A-Za-z0-9\\-\\_]+$/');
                break;
            case 'chs':
                $result = $this->regex($value, '/^[\\x{4e00}-\\x{9fa5}]+$/u');
                break;
            case 'chsAlpha':
                $result = $this->regex($value, '/^[\\x{4e00}-\\x{9fa5}a-zA-Z]+$/u');
                break;
            case 'chsAlphaNum':
                $result = $this->regex($value, '/^[\\x{4e00}-\\x{9fa5}a-zA-Z0-9]+$/u');
                break;
            case 'chsDash':
                $result = $this->regex($value, '/^[\\x{4e00}-\\x{9fa5}a-zA-Z0-9\\_\\-]+$/u');
                break;
            case 'activeUrl':
                $result = checkdnsrr($value);
                break;
            case 'ip':
                $result = $this->filter($value, [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6]);
                break;
            case 'url':
                $result = $this->filter($value, FILTER_VALIDATE_URL);
                break;
            case 'float':
                $result = $this->filter($value, FILTER_VALIDATE_FLOAT);
                break;
            case 'number':
                $result = is_numeric($value);
                break;
            case 'integer':
                $result = $this->filter($value, FILTER_VALIDATE_INT);
                break;
            case 'email':
                $result = $this->filter($value, FILTER_VALIDATE_EMAIL);
                break;
            case 'boolean':
                $result = in_array($value, [true, false, 0, 1, '0', '1'], true);
                break;
            case 'array':
                $result = is_array($value);
                break;
            case 'file':
                $result = $value instanceof File;
                break;
            case 'image':
                $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]);
                break;
            case 'token':
                $result = $this->token($value, '__token__', $data);
                break;
            default:
                if (isset(self::$type[$rule])) {
                    $result = call_user_func_array(self::$type[$rule], [$value]);
                } else {
                    $result = $this->regex($value, $rule);
                }
        }
        return $result;
    }
    protected function getImageType($image)
    {
        if (function_exists('exif_imagetype')) {
            return exif_imagetype($image);
        } else {
            $info = getimagesize($image);
            return $info[2];
        }
    }
    protected function activeUrl($value, $rule)
    {
        if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) {
            $rule = 'MX';
        }
        return checkdnsrr($value, $rule);
    }
    protected function ip($value, $rule)
    {
        if (!in_array($rule, ['ipv4', 'ipv6'])) {
            $rule = 'ipv4';
        }
        return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]);
    }
    protected function fileExt($file, $rule)
    {
        if (!$file instanceof File) {
            return false;
        }
        if (is_string($rule)) {
            $rule = explode(',', $rule);
        }
        if (is_array($file)) {
            foreach ($file as $item) {
                if (!$item->checkExt($rule)) {
                    return false;
                }
            }
            return true;
        } else {
            return $file->checkExt($rule);
        }
    }
    protected function fileMime($file, $rule)
    {
        if (!$file instanceof File) {
            return false;
        }
        if (is_string($rule)) {
            $rule = explode(',', $rule);
        }
        if (is_array($file)) {
            foreach ($file as $item) {
                if (!$item->checkMime($rule)) {
                    return false;
                }
            }
            return true;
        } else {
            return $file->checkMime($rule);
        }
    }
    protected function fileSize($file, $rule)
    {
        if (!$file instanceof File) {
            return false;
        }
        if (is_array($file)) {
            foreach ($file as $item) {
                if (!$item->checkSize($rule)) {
                    return false;
                }
            }
            return true;
        } else {
            return $file->checkSize($rule);
        }
    }
    protected function image($file, $rule)
    {
        if (!$file instanceof File) {
            return false;
        }
        if ($rule) {
            $rule = explode(',', $rule);
            list($width, $height, $type) = getimagesize($file->getRealPath());
            if (isset($rule[2])) {
                $imageType = strtolower($rule[2]);
                if ('jpeg' == $imageType) {
                    $imageType = 'jpg';
                }
                if (image_type_to_extension($type, false) != $imageType) {
                    return false;
                }
            }
            list($w, $h) = $rule;
            return $w == $width && $h == $height;
        } else {
            return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]);
        }
    }
    protected function method($value, $rule)
    {
        $method = Request::instance()->method();
        return strtoupper($rule) == $method;
    }
    protected function dateFormat($value, $rule)
    {
        $info = date_parse_from_format($rule, $value);
        return 0 == $info['warning_count'] && 0 == $info['error_count'];
    }
    protected function unique($value, $rule, $data, $field)
    {
        if (is_string($rule)) {
            $rule = explode(',', $rule);
        }
        if (false !== strpos($rule[0], '\\')) {
            $db = new $rule[0]();
        } else {
            try {
                $db = Loader::model($rule[0]);
            } catch (ClassNotFoundException $e) {
                $db = Db::name($rule[0]);
            }
        }
        $key = isset($rule[1]) ? $rule[1] : $field;
        if (strpos($key, '^')) {
            $fields = explode('^', $key);
            foreach ($fields as $key) {
                $map[$key] = $data[$key];
            }
        } elseif (strpos($key, '=')) {
            parse_str($key, $map);
        } else {
            $map[$key] = $data[$field];
        }
        $pk = strval(isset($rule[3]) ? $rule[3] : $db->getPk());
        if (isset($rule[2])) {
            $map[$pk] = ['neq', $rule[2]];
        } elseif (isset($data[$pk])) {
            $map[$pk] = ['neq', $data[$pk]];
        }
        if ($db->where($map)->field($pk)->find()) {
            return false;
        }
        return true;
    }
    protected function behavior($value, $rule, $data)
    {
        return Hook::exec($rule, '', $data);
    }
    protected function filter($value, $rule)
    {
        if (is_string($rule) && strpos($rule, ',')) {
            list($rule, $param) = explode(',', $rule);
        } elseif (is_array($rule)) {
            $param = isset($rule[1]) ? $rule[1] : null;
            $rule = $rule[0];
        } else {
            $param = null;
        }
        return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param);
    }
    protected function requireIf($value, $rule, $data)
    {
        list($field, $val) = explode(',', $rule);
        if ($this->getDataValue($data, $field) == $val) {
            return !empty($value);
        } else {
            return true;
        }
    }
    protected function requireCallback($value, $rule, $data)
    {
        $result = call_user_func_array($rule, [$value, $data]);
        if ($result) {
            return !empty($value);
        } else {
            return true;
        }
    }
    protected function requireWith($value, $rule, $data)
    {
        $val = $this->getDataValue($data, $rule);
        if (!empty($val)) {
            return !empty($value);
        } else {
            return true;
        }
    }
    protected function in($value, $rule)
    {
        return in_array($value, is_array($rule) ? $rule : explode(',', $rule));
    }
    protected function notIn($value, $rule)
    {
        return !in_array($value, is_array($rule) ? $rule : explode(',', $rule));
    }
    protected function between($value, $rule)
    {
        if (is_string($rule)) {
            $rule = explode(',', $rule);
        }
        list($min, $max) = $rule;
        return $value >= $min && $value <= $max;
    }
    protected function notBetween($value, $rule)
    {
        if (is_string($rule)) {
            $rule = explode(',', $rule);
        }
        list($min, $max) = $rule;
        return $value < $min || $value > $max;
    }
    protected function length($value, $rule)
    {
        if (is_array($value)) {
            $length = count($value);
        } elseif ($value instanceof File) {
            $length = $value->getSize();
        } else {
            $length = mb_strlen((string) $value);
        }
        if (strpos($rule, ',')) {
            list($min, $max) = explode(',', $rule);
            return $length >= $min && $length <= $max;
        } else {
            return $length == $rule;
        }
    }
    protected function max($value, $rule)
    {
        if (is_array($value)) {
            $length = count($value);
        } elseif ($value instanceof File) {
            $length = $value->getSize();
        } else {
            $length = mb_strlen((string) $value);
        }
        return $length <= $rule;
    }
    protected function min($value, $rule)
    {
        if (is_array($value)) {
            $length = count($value);
        } elseif ($value instanceof File) {
            $length = $value->getSize();
        } else {
            $length = mb_strlen((string) $value);
        }
        return $length >= $rule;
    }
    protected function after($value, $rule)
    {
        return strtotime($value) >= strtotime($rule);
    }
    protected function before($value, $rule)
    {
        return strtotime($value) <= strtotime($rule);
    }
    protected function expire($value, $rule)
    {
        if (is_string($rule)) {
            $rule = explode(',', $rule);
        }
        list($start, $end) = $rule;
        if (!is_numeric($start)) {
            $start = strtotime($start);
        }
        if (!is_numeric($end)) {
            $end = strtotime($end);
        }
        return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end;
    }
    protected function allowIp($value, $rule)
    {
        return in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule));
    }
    protected function denyIp($value, $rule)
    {
        return !in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule));
    }
    protected function regex($value, $rule)
    {
        if (isset($this->regex[$rule])) {
            $rule = $this->regex[$rule];
        }
        if (0 !== strpos($rule, '/') && !preg_match('/\\/[imsU]{0,4}$/', $rule)) {
            $rule = '/^' . $rule . '$/';
        }
        return 1 === preg_match($rule, (string) $value);
    }
    protected function token($value, $rule, $data)
    {
        $rule = !empty($rule) ? $rule : '__token__';
        if (!isset($data[$rule]) || !Session::has($rule)) {
            return false;
        }
        if (isset($data[$rule]) && Session::get($rule) === $data[$rule]) {
            Session::delete($rule);
            return true;
        }
        Session::delete($rule);
        return false;
    }
    public function getError()
    {
        return $this->error;
    }
    protected function getDataValue($data, $key)
    {
        if (strpos($key, '.')) {
            list($name1, $name2) = explode('.', $key);
            $value = isset($data[$name1][$name2]) ? $data[$name1][$name2] : null;
        } else {
            $value = isset($data[$key]) ? $data[$key] : null;
        }
        return $value;
    }
    protected function getRuleMsg($attribute, $title, $type, $rule)
    {
        if (isset($this->message[$attribute . '.' . $type])) {
            $msg = $this->message[$attribute . '.' . $type];
        } elseif (isset($this->message[$attribute][$type])) {
            $msg = $this->message[$attribute][$type];
        } elseif (isset($this->message[$attribute])) {
            $msg = $this->message[$attribute];
        } elseif (isset(self::$typeMsg[$type])) {
            $msg = self::$typeMsg[$type];
        } else {
            $msg = $title . '规则错误';
        }
        if (is_string($msg) && 0 === strpos($msg, '{%')) {
            $msg = Lang::get(substr($msg, 2, -1));
        }
        if (is_string($msg) && is_scalar($rule) && false !== strpos($msg, ':')) {
            if (is_string($rule) && strpos($rule, ',')) {
                $array = array_pad(explode(',', $rule), 3, '');
            } else {
                $array = array_pad([], 3, '');
            }
            $msg = str_replace([':attribute', ':rule', ':1', ':2', ':3'], [$title, (string) $rule, $array[0], $array[1], $array[2]], $msg);
        }
        return $msg;
    }
    protected function getScene($scene = '')
    {
        if (empty($scene)) {
            $scene = $this->currentScene;
        }
        if (!empty($scene) && isset($this->scene[$scene])) {
            $scene = $this->scene[$scene];
            if (is_string($scene)) {
                $scene = explode(',', $scene);
            }
        } else {
            $scene = [];
        }
        return $scene;
    }
    public static function __callStatic($method, $params)
    {
        $class = self::make();
        if (method_exists($class, $method)) {
            return call_user_func_array([$class, $method], $params);
        } else {
            throw new \BadMethodCallException('method not exists:' . __CLASS__ . '->' . $method);
        }
    }
}